"""Classes to interface to a real or simulated radio.

Copyright (C) Sarah Mount, 2008.

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
"""

class RadioInterface(object):
    """Interface to a real or simulated radio.
    """
    def __init__(self):
        return
    def radioSend(self, pkt, arc_colour):
        """Send an individual packet.
        Must be overridden in all subclasses.
        """
        return


class GumstixRadio(RadioInterface):
    """Class to implement radio communication on the Gumstix Verdex platform.

    TODO: Fill in these stubs.
    """
    def __init__(self):
        RadioInterface.__init__(self)
        return
    def radioSend(self, pkt, arc_colour):
        """Send an individual packet.
	TODO: Write this. Probably as a wrapper around a C function.
        """
        return


class VirtualRadio(RadioInterface):
    """Class to model radio transmission.

    TODO: Should get physical data from radio propagation model.
    """
    def __init__(self, sim, rf_ranger):
        """Initialise a new Ether object.

        @type sim: Dingo.simulation.Simulation
        @param sim: The simulation the ether is attached to
        @type rf_ranger: Dingo.ranger.RFRanger
        @param rf_ranger: The ranger used by the (simulated) transmission medium
        """        
        self.sim = sim # typeof(sim) = Simulation
        self.rf_ranger = rf_ranger 
        self.RF_radius = 100
        return
        
    def get_sensor(self, sensor_id):
        """Return a sensor within the simulation

        @rtype: Dingo.sensor.Sensor
        """
        return self.sim.get_sensor(sensor_id)

    def send(self, msg, arc_colour):
        """Send a message to a single sensor.
        
        This places a message on the receive Queue of a given sensor.
        
	If a sensor tries to send to an unreachable peer a message is
	placed in the panel of the debug plugin to log the attempt.

        @type msg: Dingo.packet.Packet
        @param msg: The message to send
        @type arc_colour: Dingo.OGL.SensorColour
        @param arc_colour: The colour of the transmission arc shown on the graph
        """
        ## Get sensors
        sender = self.get_sensor(msg.sender_id)
        dest = self.get_sensor(msg.receiver_id)
        ## Get nodes and locations
        s_node = sender.node
        (x1, y1) = (s_node.GetX(), s_node.GetY())
        r_node = dest.node
        (x2, y2) = (r_node.GetX(), r_node.GetY())

        isdropped = False

	## Check reachability
        if self.__dist((x1,y1), (x2,y2)) > self.RF_radius:
            sender.debug('RADIO: Tried to send to unreachable sensor' + msg.receiver_id)
            isdropped = True

        if not isdropped:
            ## Set RSSI
            msg.rssi = self.rf_ranger.get_rssi((x1, y1), (x2, y2))
            ## Set timestamp and time-of-flight
            msg.time_of_flight = self.rf_ranger.get_tof((x1, y1), (x2, y2))
            msg.time_received = msg.time_sent + msg.time_of_flight
            ## Send
            try:
            	dest.recv_q.put(msg)
            except Queue.Full:
            	isdropped = True
        
        ## Packet logging
        if self.sim.packetLogger.logging and not isdropped:
            self.sim.packetLogger.write(msg.sender_id,
                                        msg.receiver_id,
                                        msg.time_sent,
                                        msg.time_received,
                                        msg.time_of_flight,
                                        msg.rssi
                                       )

	## Tell the simulator to draw an arc
	attribs = {'sender':sender.node, 'receivers':[dest.node],
                   'bcast':False, 'colour':arc_colour, 'isdropped':isdropped}
	Publisher().sendMessage((consts.RADIO_TOPIC), attribs)

        return


    def broadcast(self, sender_id, arc_colour, time_sent=None, msg=None):
        """Broadcast a message to sensors within radius, excluding
        sender.
        Broadcast radius is set in api.Ether.RF_radius.
        If no message is passed to broadcast it tries to find one
        on the send_q of the sending sensor. If that queue is empty
        broadcast returns.
        Messages that are sent are shallow copies of the original
        message.

        @type sender_id: str
        @param sender_id: The ID of the sending sensor
        @type arc_colour: Dingo.OGL.SensorColour
        @param arc_colour: The colour the message arcs are on the sensorGraph object
        @type time_sent: float
        @param time_sent: Time the message was sent
        @type msg: Dingo.packet.Packet
        @param msg: The message to transmit.
        """
        sender = self.get_sensor(sender_id)
        if msg == None:
            sender = self.get_sensor(sender_id)
            if not sender.send_q.empty():
                msg = sender.send_q.get()
            else :
                return
        destnodes = []
        for r in self.reachable_sensors(sender_id):
            self.__send(copy.copy(msg), sender_id, r)
            destnodes.append(self.get_sensor(r).node)

	attribs = {'sender':sender.node, 'receivers':destnodes,
                   'bcast':True, 'colour':arc_colour, 'isdropped':False}

	Publisher().sendMessage((consts.RADIO_TOPIC), attribs)
	
        return

    def multicast(self, sender_id, arc_colour, time_sent=None, msg=None):
        """Multicast a message to the specified sensors.
        
        Multicast allows the user to send a message to a list of specified sensors, providing these sensors
        are within the range defined in the Ether.  
        
        The list of sensors to send the message to is derived from the msg packets receiver_id field.
        This should be a list or tuple containing the intended receivers ID's
        
        As per Broadcast messages are if no message is specified a
        check is made to the Queue. Messages that are sent are shallow
        copies of the original message.

	Contributed by Daniel Goldsmith.

        @type sender_id: str
        @param sender_id: The ID of the sending sensor
        @type arc_colour: Dingo.OGL.SensorColour
        @param arc_colour: The colour the message arcs are on the sensorGraph object
        @type time_sent: float
        @param time_sent: Time the message was sent
        @type msg: Dingo.packet.Packet
        @param msg: The message to transmit
        """
        sender = self.get_sensor(sender_id)
        destination = msg.receiver_id # type list

        if msg == None:
            sender = self.get_sensor(sender_id)
            if not sender.send_q.empty():
                msg = sender.send_q.get()
            else :
                return
        reachable = self.reachable_sensors(sender_id)
        finalDestination = []
        for i in destination:
            if i in reachable:
                finalDestination.append(i)
        for r in finalDestination:
            self.__send(copy.copy(msg), sender_id, r)

	attribs = {'sender':sender.node,
                   'receivers':[self.get_sensor(d).node for d in finalDestination],
                   'bcast':False, 'colour':arc_colour, 'isdropped':False}

	Publisher().sendMessage((consts.RADIO_TOPIC), attribs)
        return
        

    def reachable_sensors(self, sensor_id):
        """Returns a list of all sensor ids within a given radius.
        This checks for all sensors that are within transmission range of a given node.

        @type sensor_id: str
        @param sensor_id:  The sensor we wish to have at the centre of our range calculations
        @rtype: list
        """
        reachable = []
        for i in self.sim.s_list.keys():
            if not sensor_id == i:
                s_node = (self.get_sensor(sensor_id)).node
                (x1, y1) = (s_node.GetX(), s_node.GetY())
                r_node = (self.get_sensor(i)).node
                (x2, y2) = (r_node.GetX(), r_node.GetY())
                if self.__dist((x1, y1),(x2, y2)) <= self.RF_radius:
                    reachable.append(i)
        return reachable
    

    def __send(self, msg, sender_id, recv_id):
        """Special send function for broadcasting.
	Reachability should be checked by the calling function.
        
        @type msg: Dingo.packet.Packet
        @param msg: The message to transmit
        @type sender_id: str
        @param sender_id: The ID of the sending sensor
        @type time_sent: float
        @param time_sent: Time the message was sent
        """
        ## Get sensors
        sender = self.get_sensor(sender_id)
        dest = self.get_sensor(recv_id)
        ## Get nodes and locations
        s_node = sender.node
        (x1, y1) = (s_node.GetX(), s_node.GetY())
        r_node = dest.node
        (x2, y2) = (r_node.GetX(), r_node.GetY())
        ## Set timestamp and RSSI
        msg.rssi = self.rf_ranger.get_rssi((x1, y1), (x2, y2))
        ## Set time-of-flight
        msg.time_of_flight = self.rf_ranger.get_tof((x1, y1), (x2, y2))
        msg.time_received = msg.time_sent + msg.time_of_flight
        ## Send
        try:
            dest.recv_q.put(msg)
            isdropped = False
        except Queue.Full:
            isdropped = True

        ## Packet logging
        # FIXME: Should write one single log line for a broadcast
        if self.sim.packetLogger.logging:
            self.sim.packetLogger.write(msg.sender_id,
                                        msg.receiver_id,
                                        msg.time_sent,
                                        msg.time_received,
                                        msg.time_of_flight,
                                        msg.rssi
                                       )        
        return

    
    def __dist(self, (x1, y1), (x2, y2)):
        """Determine the 2D distance between two nodes.

        @rtype: float
        """
        return math.sqrt(math.pow(x2 - x1, 2)+math.pow(y2 - y1, 2))
